﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Xml.Serialization;
using MFToolkit.Net.XBee;

namespace LaunchPad.Core
{
    public class Field : INotifyPropertyChanged
    {
        private bool _armed;
        private ObservableCollection<ProductInfo> _inventory;
        private ObservableCollection<Node> _nodes;
        private ObservableCollection<Track> _tracks;

        public Field()
        {
            _nodes = new ObservableCollection<Node>();
            _tracks = new ObservableCollection<Track>();
            Inventory = new ObservableCollection<ProductInfo>();

            _tracks.CollectionChanged += TracksCollectionChanged;

            ProductFactory.Instance.PropertyChanged += ProductFactoryPropertyChanged;

            XBeeIO.Instance.NodeDiscoveryStarting += InstanceNodeDiscoveryStarting;
            XBeeIO.Instance.NodeDiscovered += InstanceNodeDiscovered;
            XBeeIO.Instance.NodeDiscoveryComplete += InstanceNodeDiscoveryComplete;
        }

        public ObservableCollection<Node> Nodes
        {
            get { return _nodes; }

            set
            {
                if (_nodes != value)
                {
                    _nodes = value;
                    OnPropertyChanged("Nodes");
                }
            }
        }

        [XmlIgnore]
        public bool IsArmed
        {
            get { return _armed; }
            set
            {
                foreach (Node node in Nodes)
                    node.IsArmed = value;
                _armed = value;
            }
        }

        public string AudioFile { get; set; }

        public ObservableCollection<ProductInfo> Inventory
        {
            get { return _inventory; }

            set
            {
                if (_inventory != value)
                {
                    _inventory = value;
                    _inventory.CollectionChanged += InventoryCollectionChanged;
                    OnPropertyChanged("Inventory");
                }
            }
        }

        static void InventoryCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if(e.OldItems != null)
            {
                foreach(ProductInfo product in e.OldItems)
                {
                    if(product != null && product.ProductReference != null)
                        ProductFactory.Instance.RemoveProduct(product.ProductReference.Id);
                }
            }
        }

        [XmlArrayItem(typeof (EffectTrack))]
        [XmlArrayItem(typeof (TimeTrack))]
        public ObservableCollection<Track> Tracks
        {
            get { return _tracks; }

            set
            {
                if (_tracks != value)
                {
                    _tracks = value;
                    OnPropertyChanged("Tracks");
                }
            }
        }

        [XmlIgnore]
        public IEnumerable<TrackEvent> Events
        {
            get { return Tracks.SelectMany(t => t.Timeline); }
        }

        [XmlIgnore]
        public bool IsFullyAssigned
        {
            get { return Tracks.OfType<EffectTrack>().All(e => e.IsAssigned); }
        }

        [XmlIgnore]
        public bool IsFullyConnected
        {
            get { return Nodes.All(n => n.ConnectionState == ConnectionState.Connected); }
        }

        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        public void Reset()
        {
            UpdateNodes();
            UpdateEffectReferences();
            ResetEvents();
            UpdateInventory();
            XBeeIO.Instance.BeginNodeDiscovery();
        }

        private void UpdateEffectReferences()
        {
            foreach (Node node in Nodes)
                node.UpdateEffects(Tracks.OfType<EffectTrack>());
        }

        private void UpdateNodes()
        {
            foreach(EffectTrack effect in Tracks.OfType<EffectTrack>())
            {
                var nodeId = effect.NodeId;
                var node = Nodes.FirstOrDefault(n => n.SerialNumber.Value.ToString() == nodeId);

                if (node != null)
                    effect.Node = node;
            }
        }

        public void Initialize()
        {
            IsArmed = false;
            foreach (Node node in Nodes)
            {
                if (node.ConnectionState == ConnectionState.Connected)
                    node.Initialize();
            }
        }

        private void ProductFactoryPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            if (e.PropertyName == "Products")
                ProductFactory.Products.CollectionChanged += ProductsCollectionChanged;
            UpdateInventory();
        }

        private void ProductsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            UpdateInventory();
        }

        public void UpdateInventory()
        {
            var allProducts = ProductFactory.Products.Select(product => new ProductInfo(product));
            var newProducts = allProducts.Except(Inventory);
            var oldProducts = Inventory.Except(allProducts).ToList();

            var inventory = from effect in Tracks.OfType<EffectTrack>()
                            where effect.Product != null
                            group effect by effect.Product
                            into products
                            select new ProductInfo {Product = products.Key, InShow = products.Count()};

            var orderedInventory = inventory.OrderBy(product => product.Product.Name);

            /* First remove products we're not using anymore */
            foreach (ProductInfo product in oldProducts)
                Inventory.Remove(product);

            /* Update product counts */
            foreach (var product in Inventory)
            {
                var updatedProduct = orderedInventory.FirstOrDefault(p => p.Equals(product));
                product.InShow = updatedProduct != null ? updatedProduct.InShow : 0;
            }

            /* Add new products */
            foreach (ProductInfo product in newProducts)
                Inventory.Add(product);
        }

        private void TracksCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.NewItems != null)
            {
                foreach (Track track in e.NewItems.OfType<Track>())
                    track.Field = this;
            }

            if (e.OldItems != null)
            {
                foreach (Track track in e.OldItems.OfType<Track>())
                    track.Field = null;
            }

            UpdateInventory();
        }

        public void ResetEvents()
        {
            foreach (EffectTrackEvent e in Events)
                e.State = TrackEventState.Waiting;
        }

        protected void OnPropertyChanged(string property)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(property));
        }

        #region Node Discovery

        private void InstanceNodeDiscoveryStarting(object sender, EventArgs e)
        {
            foreach (Node node in Nodes)
                node.ConnectionState = ConnectionState.Updating;
        }

        private void InstanceNodeDiscovered(object sender, EventArgs e)
        {
            var nde = e as NodeDiscoverEventArgs;

            if (nde != null)
            {
                Debug.WriteLine(string.Format("Discovered {0}", nde.NodeDiscover.NodeIdentifier));

                NodeDiscover nd = nde.NodeDiscover;

                var nodes = Nodes.Where(n => n.SerialNumber != null).
                    Where(n => n.SerialNumber.Value == nd.SerialNumber.Value);

                Node node;
                if (nodes.Any())
                    node = nodes.First();
                else
                {
                    node = new Node();
                    Nodes.Add(node);
                }

                node.NodeIdentifier = nd.NodeIdentifier;
                node.SerialNumber = nd.SerialNumber;
                node.ShortAddress = nd.ShortAddress;
                node.SignalStrength = nd.SignalStrength;

                if (string.IsNullOrWhiteSpace(node.Name))
                    node.Name = node.NodeIdentifier;

                node.ConnectionState = ConnectionState.Connected;
            }
        }

        private void InstanceNodeDiscoveryComplete(object sender, EventArgs e)
        {
            foreach (Node node in Nodes)
            {
                if (node.ConnectionState != ConnectionState.Connected)
                    node.ConnectionState = ConnectionState.Disconnected;
            }

            UpdateNodes();
        }

        #endregion
    }
}